﻿namespace Codeblack.TfsScrumboard.Runtime
{
    using System.IO;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml.Linq;
    using System.Xml.XPath;
    using System.Web;
    using Codeblack.TfsScrumboard.Resources;

    /// <summary>
    /// A stream that is to be used as a response-filter for adding tabs to the tab-header of TFS Web Access.
    /// </summary>
    public class WebAccessTabExtenderFilter : Stream
    {
        #region Private fields

        private readonly Stream responseStream;
        private readonly HttpApplication httpApplication;
        private readonly StringBuilder responseHtml;

        #endregion

        #region Constructor

        public WebAccessTabExtenderFilter(Stream responseStream, HttpApplication httpApplication)
        {
            this.responseStream = responseStream;
            this.httpApplication = httpApplication;

            this.responseHtml = new StringBuilder();
        }

        #endregion

        #region Overriden properties

        /// <summary>
        /// Gets a value indicating whether the current stream supports reading.
        /// </summary>
        /// <returns>true if the stream supports reading; otherwise, false.</returns>
        public override bool CanRead
        {
            get { return true; }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports seeking.
        /// </summary>
        /// <returns>true if the stream supports seeking; otherwise, false.</returns>
        public override bool CanSeek
        {
            get { return true; }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports writing.
        /// </summary>
        /// <returns>true if the stream supports writing; otherwise, false</returns>
        public override bool CanWrite
        {
            get { return true; }
        }

        /// <summary>
        /// Gets the length in bytes of the stream.
        /// </summary>
        /// <returns>A long value representing the length of the stream in bytes.</returns>
        public override long Length
        {
            get { return (long)this.responseHtml.Length; }
        }

        /// <summary>
        /// Gets or sets the position within the current stream.
        /// </summary>
        /// <returns>The current position within the stream.</returns>
        public override long Position { get; set; }

        #endregion

        #region Overriden methods

        /// <summary>
        /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream.
        /// </summary>
        public override void Close()
        {
            string html = this.responseHtml.ToString();

            string extendedHtml = this.ExtendTabs(html);

            byte[] extendedHtmlBytes = Encoding.UTF8.GetBytes(extendedHtml);
            this.responseStream.Write(extendedHtmlBytes, 0, extendedHtmlBytes.Length);

            this.responseStream.Close();
        }

        /// <summary>
        /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
        /// </summary>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        public override void Flush()
        {
            this.responseStream.Flush();
        }

        /// <summary>
        /// Writes a sequence of bytes to the current stream and advances the current position
        /// within this stream by the number of bytes written.
        /// </summary>
        /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
        /// <param name="count">The number of bytes to be written to the current stream.</param>
        /// <exception cref="System.ArgumentException">The sum of offset and count is greater than the buffer length.</exception>
        /// <exception cref="System.ArgumentNullException">buffer is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">offset or count is negative.</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support writing.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override void Write(byte[] buffer, int offset, int count)
        {
            string html = Encoding.UTF8.GetString(buffer, offset, count);

            this.responseHtml.Append(html);
        }

        /// <summary>
        /// When overridden in a derived class, reads a sequence of bytes from the current stream
        /// and advances the position within the stream by the number of bytes read.
        /// </summary>
        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains
        /// the specified byte array with the values between offset and (offset + count - 1)
        /// replaced by the bytes read from the current source.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin storing the
        /// data read from the current stream.</param>
        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
        /// <returns>The total number of bytes read into the buffer. This can be less than the
        /// number of bytes requested if that many bytes are not currently available, or zero (0)
        /// if the end of the stream has been reached.</returns>
        /// <exception cref="System.ArgumentException">The sum of offset and count is larger than the buffer length.</exception>
        /// <exception cref="System.ArgumentNullException">buffer is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">offset or count is negative.</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support reading.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override int Read(byte[] buffer, int offset, int count)
        {
            return 0;
        }

        /// <summary>
        /// Sets the position within the current stream.
        /// </summary>
        /// <param name="offset">A byte offset relative to the origin parameter.</param>
        /// <param name="origin">A value of type System.IO.SeekOrigin indicating the reference
        /// point used to obtain the new position.</param>
        /// <returns>The new position within the current stream.</returns>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support seeking,
        /// such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override long Seek(long offset, SeekOrigin origin)
        {
            return 0L;
        }

        /// <summary>
        /// Sets the length of the current stream.
        /// </summary>
        /// <param name="length">The desired length of the current stream in bytes.</param>
        /// <exception cref="System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support both writing and seeking,
        /// such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override void SetLength(long length)
        {
        }

        #endregion

        #region Private methods

        private static void AppendTab(XElement xLastTabFirstRow, XElement xLastTabSecondRow, string title, string pageUrl, string pguid, string applicationPath, string absolutePath)
        {
            // create the full absolute url for the tab
            string url = string.Format("{0}{1}", applicationPath, pageUrl);
            // determine if the tab is active
            bool isActive = absolutePath == url.ToLowerInvariant();

            // append the pguid to the absolute url, if available
            if (!string.IsNullOrEmpty(pguid))
            {
                url += "?" + pguid;
            }

            // determine the css-class of the tab
            string tabClass = isActive ? "att" : "ptt";

            // add the tab itself
            xLastTabFirstRow.AddAfterSelf(new XElement("td", new XAttribute("class", tabClass),
                                                      new XElement("a", new XAttribute("class", tabClass),
                                                                        new XAttribute("href", url),
                                                                        new XElement("div", title, new XAttribute("class", tabClass)))));

            // add the bottom of the tab
            xLastTabSecondRow.AddAfterSelf(new XElement("td", new XAttribute("class", isActive ? "atb" : "ptb")));
        }

        private string ExtendTabs(string html)
        {
            XElement xTable = null;
            string pguid = string.Empty;

            // find the table that represents the tabs in the html
            Regex regex = new Regex("<table.*class=\"mainTab\".*</table>", RegexOptions.IgnoreCase);
            Match tableMatch = regex.Match(html);
            if (tableMatch.Success)
            {
                // load the tabs-html as an xml document and find the last element in each row
                xTable = XElement.Parse(tableMatch.Value);

                // determine the pguid from any of the existing tabs
                Match pguidMatch = Regex.Match(tableMatch.Value, @"pguid=\w{8}-\w{4}-\w{4}-\w{4}-\w{12}", RegexOptions.IgnoreCase);
                if (pguidMatch.Success)
                {
                    pguid = pguidMatch.Value;
                }
            }

            if (xTable != null)
            {
                string absolutePath = HttpContext.Current.Request.Url.AbsolutePath.ToLowerInvariant();
                string applicationPath = HttpContext.Current.Request.ApplicationPath;

                // find the last elements in each row, tabs will be added before
                XElement xLastTabFirstRow = xTable.XPathSelectElement("tr[1]/td[last()-1]");
                XElement xLastTabSecondRow = xTable.XPathSelectElement("tr[2]/td[last()-1]");

                if (xLastTabFirstRow != null && xLastTabSecondRow != null)
                {
                    // create the tasks tab and add it to the tabs
                    AppendTab(xLastTabFirstRow, xLastTabSecondRow, ScrumboardResources.TasksPageTitle, ScrumboardResources.TasksPageUrl, pguid, applicationPath, absolutePath);

                    // create the planning tab and add it to the tabs
                    AppendTab(xLastTabFirstRow, xLastTabSecondRow, ScrumboardResources.PlanningPageTitle, ScrumboardResources.PlanningPageUrl, pguid, applicationPath, absolutePath);

                    // replace the original table with the new table
                    html = regex.Replace(html, xTable.ToString());
                }
            }

            return html;
        }

        #endregion
    }
}
